package ac.technion.StreamMania;

import java.io.FileNotFoundException;
import java.io.IOException;

import util.HashMap;
import util.Iterator;
import util.Map;
import ac.technion.StreamMania.application.controller.uilisteners.StackBuilder;
import ac.technion.StreamMania.application.state.Person;
import ac.technion.StreamMania.application.uielements.iface.IParticipantsUIElement;
import ac.technion.StreamMania.connectivity.NetworkAbstractionAppLayer;
import ac.technion.StreamMania.connectivity.iface.INetworkListener;
import ac.technion.gcframework.layers.Layer;
import ac.technion.gcframework.layers.ProtocolStack;
import ac.technion.gcframework.membership.Member;
import ac.technion.gcframework.membership.View;
import android.test.IsolatedContext;
import android.util.Log;

/**
 * This class handles the sender and receivers state during the session.
 * Both the sender and receiver create an instance of the class on initialization,
 * and use it to configure the protocol stack, streaming media player, network listener etc.
 * 
 * @author Arnony
 *
 */
public class StreamingHall {
	private final NetworkAbstractionAppLayer m_networkAbstractionAppLayer;
	private final ProtocolStack m_protocolStack;
	private IParticipantsUIElement m_participantsUI;
	private StreamingMediaPlayer m_StreamingMediaPlayer;
	
	private final Map m_mpPersonMap = new HashMap();
	private final Person m_self;
	private boolean senderExist=false;
	private boolean senderStopped = true;
	
	private int numListeners = 0;
	
	private String fileName = null;
	private long lengthInKb = -1;
	private long lengthInSeconds = -1;
	
	
	/**
	 * The class constructor.
	 * @param ownName - Name of the current user.
	 * @param broadcastAddress - The address to which the broadcast is performed.
	 * @param smp - The streaming media player object, used to play the file by the receivers.
	 * @param isSender - True iff current user is a sender.
	 */
	public StreamingHall(String ownName, String broadcastAddress, StreamingMediaPlayer smp, boolean isSender)
	{
		m_networkAbstractionAppLayer = new NetworkAbstractionAppLayer(ownName, new NetworkListener(), isSender);
		m_protocolStack = StackBuilder.buildStack(broadcastAddress, m_networkAbstractionAppLayer);
		m_StreamingMediaPlayer = smp;
		m_self = new Person(ownName, Layer.getInconsistentView().getSelf(), isSender);
	}
	


	/**
	 * Initializes the participants UI and the protocol stack.
	 */
	public void init(IParticipantsUIElement StreamingHallParticipantsUI)
	{
		m_participantsUI = StreamingHallParticipantsUI;
		m_protocolStack.init();		
	}
	
	public void initProtocolStack() 
	{
		m_protocolStack.init();		
	}
	
	/**
	 * Used by the sender to send a chunk of the media file.
	 * @param streamingData - The date to be sent.
	 */
	public void sendStreamingData(byte[] streamingData) {	
		m_networkAbstractionAppLayer.sendStreamingMessage(streamingData);	
	}
	
	/**
	 * Used by the sender to send a notice to all receivers that a new media file is about to 
	 * be streamed.
	 * @param fileName - Name of the media file.
	 * @param lengthInKb - File's length in kilobytes.
	 * @param lengthInSeconds - File's length in seconds.
	 */
	public void sendNewStreamNotice(String fileName, long lengthInKb, long lengthInSeconds) {
		senderStopped = false;
		this.fileName = fileName;
		this.lengthInKb = lengthInKb;
		this.lengthInSeconds = lengthInSeconds;
		m_networkAbstractionAppLayer.sendNewStreamMessage(fileName, lengthInKb, lengthInSeconds);
		
	}
	
	/**
	 * Used by the sender to tell all receivers to stop the media player.
	 */
	public void pauseReceivers() {
		m_networkAbstractionAppLayer.sendPause();
	}
	
	/**
	 * Used by the sender to tell all receivers to continue the media player. 
	 */
	public void continueReceivers() {
		m_networkAbstractionAppLayer.sendContinue();
	}
	
	/**
	 * Used by the sender to tell all receivers to end the current streaming session. 
	 */
	public void stopReceivers(){
		m_networkAbstractionAppLayer.sendSenderStopped();
	}
	
	/**
	 * Updates the boolean flag to indicate that the sender has stopped the streaming.
	 */
	public void setSendingStopped(){
		senderStopped = true;
	}
	
	/**
	 * @return True iff one of the participants is a sender.
	 */
	public boolean doesSenderExist()
	{
		return senderExist;
	}
	
	/**
	 * Used by the sender to update all receivers that the streaming has finished.
	 */
	public void updateSenderFinished(){
		m_networkAbstractionAppLayer.sendSenderFinished();
	}
	
	/**
	 * @return the number of receivers currently registered.
	 */
	public int getNumListeners()
	{
		return numListeners;
	}
	
	/**
	 * @return True iff the current user is a sender.
	 */
	public boolean getIsSender(){
		return m_self.getIsSender();
	}
	
	/**
	 * Quits the streaming hall 
	 */
	public void quit()
	{
		m_networkAbstractionAppLayer.quit();
		
		// Let the FW a chance to send out leave notification
		try
		{
			Thread.sleep(1300);
		}
		catch (InterruptedException e)
		{}
		
		m_protocolStack.dispose();
	}
	
	/**
	 * Retrieves the person object that's contained in the member object.
	 * @param member - The relevant member.
	 */
	private synchronized Person getPerson(Member member)
	{
		return (Person)m_mpPersonMap.get(member);
	}
	
	/**
	 * Looks for the person whose name is "name" in the participants list.
	 * @param name - The relevant name.
	 * @return - The relevant person object (or null if no such person exists)
	 */
	private synchronized Person getPersonByName(String name) {
		Iterator itPersons = m_mpPersonMap.values().iterator();
		while (itPersons.hasNext()) {
			Person person = (Person)itPersons.next();
			if (person.getName().equals(name))
				return person;
		}
		return null;
	}
	
	/**
	 * Enlists a person to the list of participants.
	 * @param person - person to be added.
	 */
	private synchronized void setPerson(Person person)
	{
		m_mpPersonMap.put(person.getMember(), person);
	}
	
	/**
	 * Removes a person from the participants list.
	 * @param member - person to be removed.
	 */
	private synchronized void removePerson(Member member)
	{
		m_mpPersonMap.remove(member);
	}

	/**
	 * This class reacts to events risen by the application layer and according to state, transfers them into
	 * actions on GUI 
	 */
	private class NetworkListener implements INetworkListener
	{
		
		/**
		 * Used by the receivers when they receive a chunk of media from the sender.
		 * @param receivedMedia - The data received.
		 * @param counter - The serial number of the received file.
		 */
		public void handleReceivedMedia(byte[] receivedMedia, int counter) {
			if(senderStopped){
				return;
			}
			try {
				m_StreamingMediaPlayer.assembleReceivedMedia(receivedMedia, counter);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		/**
		 * Used by the receivers when they receive a notice on a new streaming session.
		 * @param fileName - Name of the media file about to be streamed.
		 * @param lengthInKb - File's length in kilobytes.
		 * @param lengthInSeconds - File's length in seconds.
		 */
		public void handleNewStream(String fileName, long lengthInKb, long lengthInSeconds) {
			senderStopped = false;
			try {
				m_StreamingMediaPlayer.initForReceiver(fileName, lengthInKb, lengthInSeconds);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}
		
		/**
		 * Used by the receivers when they get the sender's request to pause the media player.
		 */
		public void handlePause() {
			m_StreamingMediaPlayer.pauseStreaming();
		}
		
		/**
		 * Used by the receivers when they get the sender's request to continue the media player.
		 */
		public void handleContinue() {
			m_StreamingMediaPlayer.continueStreaming();
		}
		
		/**
		 * Used by the receivers when they get the sender's request to end the current streaming session.
		 */
		public void handleSenderStopped(){
			senderStopped = true;
			if(m_StreamingMediaPlayer!=null){
				m_StreamingMediaPlayer.stopStreaming();
			}
			
		}
		
		/**
		 * Used by the receivers when they get the sender's notification that he's done streaming.
		 */
		public void handleSenderFinished(){
			m_StreamingMediaPlayer.updateStatusDoneStreaming();
		}

		/**
		 * called when a new participants joins the streaming hall.
		 * @param memebr - The new member.
		 * @param name - The new member's name.
		 * @param isSender - True iff the new member is a sender.
		 */
		public void memberJoined(Member member, String name, boolean isSender)
		{
			Person existingPerson = getPerson(member);
			if (existingPerson != null)
			{
				if (existingPerson.getName().equals(name))
					return;
				
				m_participantsUI.personRemoved(existingPerson);
				removePerson(member);
			}
			
			existingPerson = getPersonByName(name);
			if (existingPerson != null)
			{
				m_participantsUI.personRemoved(existingPerson);
				removePerson(existingPerson.getMember());
			}
			
			Person newPerson = new Person(name, member, isSender);
			setPerson(newPerson);	
			m_participantsUI.personAdded(newPerson);
						
			if(isSender)
			{
				senderExist=isSender;
			}
			else{
				// Adds to the number of listeners in the system for the sender.
				if(m_self.getIsSender()){
					numListeners++;
					Log.e("Listeners", "Receiver joined. Num of listeners is " + numListeners);
				}
					
			}
			
			/// The sender sends info on the streamed media to his new receiver friend */
			if (m_self.getIsSender() && !senderStopped) {
				m_networkAbstractionAppLayer.sendNewStreamMessage(fileName, lengthInKb, lengthInSeconds);
			}
		}

		/**
		 * called when a participants leaves the streaming hall.
		 * @param memebr - The leaving member.
		 */
		public void memberLeft(Member member)
		{
			Person leavingPerson = getPerson(member);
			if (leavingPerson == null)
				return;
			
			if(!leavingPerson.getIsSender() && m_self.getIsSender()){
				numListeners--;
				Log.e("Listeners", "Receiver left. Num of listeners is " + numListeners);
			}
			
			if (leavingPerson.getIsSender() && !m_self.getIsSender()) {
				senderExist = false;
				handleSenderStopped();
			}
			
			removePerson(member);	
			m_participantsUI.personRemoved(leavingPerson);
		}

		/**
		 * called when a participants has disconnected.
		 * @param memebr - The disconnecting member.
		 */
		public void memberDisconnected(Member member)
		{
			Person disconnectedPerson = getPerson(member);					
			if (disconnectedPerson == null)
				return;
			
			Log.e("Disconnection", disconnectedPerson.getName() + " Disconnected!");
			
			if(!disconnectedPerson.getIsSender() && m_self.getIsSender()){
				numListeners--;
			}
			
			if (disconnectedPerson.getIsSender()) {
				senderExist = false;			
			}
			
			m_participantsUI.personDisconnected(disconnectedPerson);
		}

		/**
		 * called when a participants has resconnected.
		 * @param memebr - The resconnecting member.
		 */
		public void memberReConnected(Member member)
		{
			Person reconnectedPerson = getPerson(member);
			if (reconnectedPerson == null)
				return;
			
			Log.e("Reconnection", reconnectedPerson.getName() + " Reconnected!");
			
			if(reconnectedPerson.getIsSender() && m_self.getIsSender()){
				int n;
				n=numListeners;
				numListeners = n;
			}
			else if (reconnectedPerson.getIsSender()) {
				senderExist = true;
			}
			else if (!reconnectedPerson.getIsSender() && m_self.getIsSender()) {
				numListeners++;
			}
			
			m_participantsUI.personReconnected(reconnectedPerson);
		}
	}

}
